using System;
using System.IO;
using System.Text;
using System.Diagnostics;

namespace OpenNETCF.Compression
{
    public class zlibCE
    {
        internal const int CHUNK = 16384;

        public static ErrorCode Compress(byte[] destination, byte[] source, out int compressedSize)
        {
            compressedSize = destination.Length;
            int sourceLen = source.Length;

            return zlib_native.compress(destination, ref compressedSize, source, sourceLen);             
        }

        public static ErrorCode Uncompress(byte[] destination, byte[] source, out int uncompressedSize)
        {
            uncompressedSize = destination.Length;
            int sourceLen = source.Length;

            return zlib_native.uncompress(destination, ref uncompressedSize, source, sourceLen);
        }

        /// <summary>
        /// Inflates gzipped data. Does not expect a gzip file header
        /// </summary>
        /// <param name="source">Raw gzipped intput data stream</param>
        /// <param name="dest">Output inflated stream</param>
        /// <returns></returns>
        unsafe public static ErrorCode Inflate(Stream source, Stream dest)
        {
            ErrorCode ret;
            uint have;
            z_stream strm = new z_stream();
            byte[] inf = new byte[CHUNK];
            byte[] outf = new byte[CHUNK];

            /* allocate inflate state */
            strm.zalloc = IntPtr.Zero;
            strm.zfree = IntPtr.Zero;
            strm.opaque = IntPtr.Zero;
            strm.avail_in = 0;
            strm.next_in = null;
            ret = zlib_native.inflateInit(ref strm);
            if (ret != ErrorCode.OK)
                return ret;
            unsafe
            {
                fixed (byte* pIn = &inf[0])
                fixed (byte* pOut = &outf[0])
                {
                    /* decompress until deflate stream ends or end of file */
                    do
                    {
                        strm.avail_in = source.Read(inf, 0, CHUNK);
                        if (strm.avail_in == 0)
                        {
                            zlib_native.inflateEnd(ref strm);
                            return ErrorCode.ErrNo;
                        }
                        if (strm.avail_in == 0)
                            break;

                        strm.next_in = pIn;

                        /* run inflate() on input until output buffer not full */
                        do
                        {
                            strm.avail_out = CHUNK;
                            strm.next_out = pOut;
                            ret = zlib_native.inflate(ref strm, zlib_native.Z_NO_FLUSH);
                            Debug.Assert(ret != ErrorCode.StreamError);  /* state not clobbered */
                            switch (ret)
                            {
                                case ErrorCode.NeedDict:
                                    zlib_native.inflateEnd(ref strm);
                                    return ErrorCode.DataError;     /* and fall through */
                                case ErrorCode.DataError:
                                case ErrorCode.MemoryError:
                                    zlib_native.inflateEnd(ref strm);
                                    return ret;
                            }
                            have = (uint)(CHUNK - (int)strm.avail_out);
                            try
                            {
                                dest.Write(outf, 0, (int)have);
                            }
                            catch (IOException)
                            {
                                zlib_native.inflateEnd(ref strm);
                                return ErrorCode.ErrNo;
                            }
                        } while (strm.avail_out == 0);

                        /* done when inflate() says it's done */
                    } while (ret != ErrorCode.StreamEnd);
                }

                /* clean up and return */
                zlib_native.inflateEnd(ref strm);
                return ret == ErrorCode.StreamEnd ? ErrorCode.OK : ErrorCode.DataError;
            }
        }

        /// <summary>
        /// Compresses input stream
        /// </summary>
        /// <param name="source">Input data</param>
        /// <param name="dest">Output stream (compressed). No header is written</param>
        /// <param name="level">Compression level</param>
        /// <returns></returns>
        static public ErrorCode Deflate(Stream source, Stream dest, int level)
        {
            ErrorCode ret;
            int flush;
            uint have;
            z_stream strm = new z_stream();
            byte[] inf = new byte[CHUNK];
            byte[] outf = new byte[CHUNK];

            /* allocate deflate state */
            strm.zalloc = IntPtr.Zero;
            strm.zfree = IntPtr.Zero;
            strm.opaque = IntPtr.Zero;
            unsafe
            {
                ret = zlib_native.deflateInit(ref strm, level);
                if (ret != ErrorCode.OK)
                    return ret;

                fixed (byte* pIn = &inf[0])
                fixed (byte* pOut = &outf[0])
                {

                    /* compress until end of file */
                    do
                    {
                        strm.avail_in = source.Read(inf, 0, CHUNK);
                        if (strm.avail_in == 0)
                        {
                            zlib_native.deflateEnd(ref strm);
                            return ErrorCode.ErrNo;
                        }

                        flush = source.Position == source.Length ? zlib_native.Z_FINISH : zlib_native.Z_NO_FLUSH;

                        strm.next_in = pIn;


                        /* run deflate() on input until output buffer not full, finish
                           compression if all of source has been read in */
                        do
                        {
                            strm.avail_out = CHUNK;
                            strm.next_out = pOut;
                            ret = zlib_native.deflate(ref strm, flush);    /* no bad return value */
                            Debug.Assert(ret != ErrorCode.StreamError);  /* state not clobbered */
                            have = (uint)(CHUNK - (int)strm.avail_out);
                            try
                            {
                                dest.Write(outf, 0, (int)have);
                            }
                            catch (IOException)
                            {
                                zlib_native.deflateEnd(ref strm);
                                return ErrorCode.ErrNo;
                            }
                        } while (strm.avail_out == 0);
                        Debug.Assert(strm.avail_in == 0);     /* all input will be used */

                        /* done when last data in file processed */
                    } while (flush != zlib_native.Z_FINISH);
                }
            }
            Debug.Assert(ret == ErrorCode.StreamEnd);        /* stream will be complete */

            /* clean up and return */
            zlib_native.deflateEnd(ref strm);
            return ErrorCode.OK;
        }
    }
}
